
import { Point, util } from 'fabric'
import { Disposable } from '../utils/lifecycle'
import { check } from '../utils/check'


const Keys = (obj) => {
  return Object.keys(obj)
}

export class FabricGuide extends Disposable {
  constructor(canvas) {
      super();
      this.canvas = canvas;
      this.aligningLineMargin = 10;
      this.aligningLineWidth = 1;
      this.aligningLineColor = '#F68066';
      this.verticalLines = [];
      this.horizontalLines = [];
      this.ignoreObjTypes = [];
      this.pickObjTypes = [];
      this.dirty = false;
      const mouseUp = () => {
          if (this.horizontalLines.length || this.verticalLines.length) {
              this.clearGuideline();
              this.clearStretLine();
          }
      };
      this.canvasEvents = {
          'before:render': this.clearGuideline.bind(this),
          'after:render': this.drawGuideLines.bind(this),
          'object:moving': this.objectMoving.bind(this),
          'mouse:up': mouseUp,
      };
      canvas.on(this.canvasEvents);
  }
  objectMoving({ target }) {
      this.clearStretLine();
      const transform = this.canvas._currentTransform;
      if (!transform)
          return;
      this.activeObj = target;
      const activeObjects = this.canvas.getActiveObjects();
      const canvasObjects = [];
      const add = (group) => {
          const objects = group.getObjects().filter((obj) => {
              if (this.ignoreObjTypes.length) {
                  return !this.ignoreObjTypes.some((item) => obj.get(item.key) === item.value);
              }
              if (this.pickObjTypes.length) {
                  return this.pickObjTypes.some((item) => obj.get(item.key) === item.value);
              }
              // 排除 自己 和 激活选区内的元素
              if (activeObjects.includes(obj)) {
                  return false;
              }
              if (!obj.visible) {
                  return false;
              }
              // 元素为组，把组内元素加入，同时排除组本身
              if (check.isActiveSelection(obj)) {
                  add(obj);
                  return false;
              }
              // 元素为组，把组内元素加入，同时排除组本身
              if (check.isCollection(obj) && target.group && obj === target.group) {
                  add(obj);
                  return false;
              }
              return true;
          });
          canvasObjects.push(...objects);
      };
      if (check.isActiveSelection(target)) {
          const needAddParent = new Set();
          target.forEachObject((obj) => {
              const parent = obj.getParent();
              if (parent)
                  needAddParent.add(parent);
          });
          needAddParent.forEach((parent) => {
              if (check.isNativeGroup(parent)) {
                  canvasObjects.push(parent);
              }
              add(parent);
          });
      }
      else {
          const parent = target.getParent();
          if (check.isNativeGroup(parent)) {
              canvasObjects.push(parent);
          }
          add(parent);
      }
      this.traversAllObjects(target, canvasObjects);
  }
  clearStretLine() {
      this.verticalLines.length = this.horizontalLines.length = 0;
  }
  getObjDraggingObjCoords(activeObject) {
      const coords = this.getCoords(activeObject);
      const centerPoint = this.calcCenterPointByACoords(coords).subtract(activeObject.getCenterPoint());
      const newCoords = Keys(coords).map((key) => coords[key].subtract(centerPoint));
      return {
          tl: newCoords[0],
          tr: newCoords[1],
          br: newCoords[2],
          bl: newCoords[3],
          c: activeObject.getCenterPoint(),
      };
  }
  getObjMaxWidthHeightByCoords(coords) {
      const { c, tl, tr } = coords;
      const objHeight = Math.max(Math.abs(c.y - tl.y), Math.abs(c.y - tr.y)) * 2;
      const objWidth = Math.max(Math.abs(c.x - tl.x), Math.abs(c.x - tr.x)) * 2;
      return { objHeight, objWidth };
  }
  // 当对象被旋转时，需要忽略一些坐标，例如水平辅助线只取最上、下边的坐标（参考 figma）
  omitCoords(objCoords, type) {
      const newCoords = objCoords;
      const axis = type === 'vertical' ? 'x' : 'y';
      Keys(objCoords).forEach((key) => {
          if (objCoords[key][axis] < newCoords.tl[axis]) {
              newCoords[key] = objCoords[key];
          }
          if (objCoords[key][axis] > newCoords.tl[axis]) {
              newCoords[key] = objCoords[key];
          }
      });
      return newCoords;
  }
  /**
   * 检查 value1 和 value2 是否在指定的范围内，用于对齐线的计算
   */
  isInRange(value1, value2) {
      return (Math.abs(Math.round(value1) - Math.round(value2)) <=
        this.aligningLineMargin / this.canvas.getZoom());
  }
  getCoords(obj) {
      const [tl, tr, br, bl] = obj.getCoords(true);
      return { tl, tr, br, bl };
  }
  /**
   * fabric.Object.getCenterPoint will return the center point of the object calc by mouse moving & dragging distance.
   * calcCenterPointByACoords will return real center point of the object position.
   */
  calcCenterPointByACoords(coords) {
      return new Point((coords.tl.x + coords.br.x) / 2, (coords.tl.y + coords.br.y) / 2);
  }
  traversAllObjects(activeObject, canvasObjects) {
      const objCoordsByMovingDistance = this.getObjDraggingObjCoords(activeObject);
      const snapXPoints = new Set();
      const snapYPoints = new Set();
      for (let i = canvasObjects.length; i--;) {
          const objCoords = Object.assign(Object.assign({}, this.getCoords(canvasObjects[i])), { c: canvasObjects[i].getCenterPoint() });
          const { objHeight, objWidth } = this.getObjMaxWidthHeightByCoords(objCoords);
          Keys(objCoordsByMovingDistance).forEach((activeObjPoint) => {
              const newCoords = canvasObjects[i].angle !== 0 ? this.omitCoords(objCoords, 'horizontal') : objCoords;
              function calcHorizontalLineCoords(objPoint, activeObjCoords) {
                  let x1, x2;
                  if (objPoint === 'c') {
                      x1 = Math.min(objCoords.c.x - objWidth / 2, activeObjCoords[activeObjPoint].x);
                      x2 = Math.max(objCoords.c.x + objWidth / 2, activeObjCoords[activeObjPoint].x);
                  }
                  else {
                      x1 = Math.min(objCoords[objPoint].x, activeObjCoords[activeObjPoint].x);
                      x2 = Math.max(objCoords[objPoint].x, activeObjCoords[activeObjPoint].x);
                  }
                  return { x1, x2 };
              }
              Keys(newCoords).forEach((objPoint) => {
                  if (this.isInRange(objCoordsByMovingDistance[activeObjPoint].y, objCoords[objPoint].y)) {
                      const y = objCoords[objPoint].y;
                      const offset = objCoordsByMovingDistance[activeObjPoint].y - y;
                      snapYPoints.add(objCoordsByMovingDistance.c.y - offset);
                      const aCoords = this.getCoords(activeObject);
                      const { x1, x2 } = calcHorizontalLineCoords(objPoint, Object.assign(Object.assign({}, aCoords), { c: this.calcCenterPointByACoords(aCoords) }));
                      this.horizontalLines.push({ y, x1, x2 });
                  }
              });
          });
          Keys(objCoordsByMovingDistance).forEach((activeObjPoint) => {
              const newCoords = canvasObjects[i].angle !== 0 ? this.omitCoords(objCoords, 'vertical') : objCoords;
              function calcVerticalLineCoords(objPoint, activeObjCoords) {
                  let y1, y2;
                  if (objPoint === 'c') {
                      y1 = Math.min(newCoords.c.y - objHeight / 2, activeObjCoords[activeObjPoint].y);
                      y2 = Math.max(newCoords.c.y + objHeight / 2, activeObjCoords[activeObjPoint].y);
                  }
                  else {
                      y1 = Math.min(objCoords[objPoint].y, activeObjCoords[activeObjPoint].y);
                      y2 = Math.max(objCoords[objPoint].y, activeObjCoords[activeObjPoint].y);
                  }
                  return { y1, y2 };
              }
              Keys(newCoords).forEach((objPoint) => {
                  if (this.isInRange(objCoordsByMovingDistance[activeObjPoint].x, objCoords[objPoint].x)) {
                      const x = objCoords[objPoint].x;
                      const offset = objCoordsByMovingDistance[activeObjPoint].x - x;
                      snapXPoints.add(objCoordsByMovingDistance.c.x - offset);
                      const aCoords = this.getCoords(activeObject);
                      const { y1, y2 } = calcVerticalLineCoords(objPoint, Object.assign(Object.assign({}, aCoords), { c: this.calcCenterPointByACoords(aCoords) }));
                      this.verticalLines.push({ x, y1, y2 });
                  }
              });
          });
      }
      this.snap({
          activeObject,
          draggingObjCoords: objCoordsByMovingDistance,
          snapXPoints,
          snapYPoints,
      });
  }
  /**
   * 自动吸附对象
   */
  snap({ activeObject, draggingObjCoords, snapXPoints, snapYPoints, }) {
      if (snapXPoints.size === 0 && snapYPoints.size === 0)
          return;
      // 获得最近的吸附点
      const sortPoints = (list, originPoint) => {
          if (list.size === 0) {
              return originPoint;
          }
          const sortedList = [...list].sort((a, b) => Math.abs(originPoint - a) - Math.abs(originPoint - b));
          return sortedList[0];
      };
      // auto snap nearest object, record all the snap points, and then find the nearest one
      activeObject.setXY(new Point(sortPoints(snapXPoints, draggingObjCoords.c.x), sortPoints(snapYPoints, draggingObjCoords.c.y)), 'center', 'center');
  }
  drawSign(x, y) {
      const ctx = this.canvas.getTopContext();
      ctx.strokeStyle = this.aligningLineColor;
      ctx.beginPath();
      const size = 3;
      ctx.moveTo(x - size, y - size);
      ctx.lineTo(x + size, y + size);
      ctx.moveTo(x + size, y - size);
      ctx.lineTo(x - size, y + size);
      ctx.stroke();
  }
  drawLine(x1, y1, x2, y2) {
      const ctx = this.canvas.getTopContext();
      const point1 = util.transformPoint(new Point(x1, y1), this.canvas.viewportTransform);
      const point2 = util.transformPoint(new Point(x2, y2), this.canvas.viewportTransform);
      // use origin canvas api to draw guideline
      ctx.save();
      ctx.lineWidth = this.aligningLineWidth;
      ctx.strokeStyle = this.aligningLineColor;
      ctx.beginPath();
      ctx.moveTo(point1.x, point1.y);
      ctx.lineTo(point2.x, point2.y);
      ctx.stroke();
      this.drawSign(point1.x, point1.y);
      this.drawSign(point2.x, point2.y);
      ctx.restore();
      this.dirty = true;
  }
  drawVerticalLine(coords, movingCoords) {
      if (!Object.values(movingCoords).some((coord) => Math.abs(coord.x - coords.x) < 0.0001))
          return;
      this.drawLine(coords.x, Math.min(coords.y1, coords.y2), coords.x, Math.max(coords.y1, coords.y2));
  }
  drawHorizontalLine(coords, movingCoords) {
      if (!Object.values(movingCoords).some((coord) => Math.abs(coord.y - coords.y) < 0.0001))
          return;
      this.drawLine(Math.min(coords.x1, coords.x2), coords.y, Math.max(coords.x1, coords.x2), coords.y);
  }
  drawGuideLines(e) {
      if (!e.ctx || (!this.verticalLines.length && !this.horizontalLines.length) || !this.activeObj) {
          return;
      }
      const movingCoords = this.getObjDraggingObjCoords(this.activeObj);
      for (let i = this.verticalLines.length; i--;) {
          this.drawVerticalLine(this.verticalLines[i], movingCoords);
      }
      for (let i = this.horizontalLines.length; i--;) {
          this.drawHorizontalLine(this.horizontalLines[i], movingCoords);
      }
      // this.canvas.calcOffset()
  }
  clearGuideline() {
      if (!this.dirty)
          return;
      this.dirty = false;
      this.canvas.clearContext(this.canvas.getTopContext());
  }
  dispose() {
      super.dispose();
      this.canvas.off(this.canvasEvents);
  }
}
